package com.googlecode.gamedroid;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;

import com.googlecode.gamedroid.event.Event;
import com.googlecode.gamedroid.event.EventReceiver;
import com.googlecode.gamedroid.event.source.EventSource;
import com.googlecode.gamedroid.gamepad.GamePad;
import com.googlecode.gamedroid.sockethelper.ISocket;

public class Connection extends EventReceiver
{
	private class ReaderThread extends Thread
	{
		public ReaderThread()
		{
			super("Connection.ReaderThread");
		}
		
		private void onError()
		{
			mGamepad.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					Connection.this.onError();
				}
			});
		}
		
		private void onReceive(final String data)
		{
			mGamepad.runOnUiThread(new Runnable()
			{
				@Override
				public void run()
				{
					Connection.this.onReceive(data);
				}
			});
		}
		
		@Override
		public void run()
		{
			InputStream stream;
			try
			{
				stream = mSocket.getInputStream();
				String data = "";
				byte[] buffer = new byte[1024];
				
				while(true)
				{
					stream.read(buffer);
					data += new String(buffer);
					if(0 == stream.available())
					{
						this.onReceive(data);
						data = "";
					}
				}
			}
			catch(IOException e)
			{
				this.onError();
			}
		}
	}
	
	public class ConnectionError extends Exception
	{
		private static final long serialVersionUID = -4232805929389755149L;
	}
	
	public class StatusException extends Exception
	{
		private static final long serialVersionUID = -4782534870373514118L;
		
		public StatusException()
		{
			super();
		}
		
		public StatusException(String string)
		{
			super(string);
		}
	}
	
	private GamePad mGamepad = null;
	private ISocket mSocket = null;
	private ArrayList<EventSource> mSources = new ArrayList<EventSource>();
	private ConnectionStatus mStatus = ConnectionStatus.NotConnected;
	protected ReaderThread mReaderThread;
	
	private void onError()
	{
		if(mStatus == ConnectionStatus.NotConnected)
			// Service has been stopped , don't throw any more errors
			return;
		
		if(mGamepad != null)
			mGamepad.onConnectionLost();
		
		if(mStatus == ConnectionStatus.Connecting)
			// Service is connecting, we can't stop here. The connecting thread will
			// call onError itself.
			return;
		
		try
		// StatusException will only be thrown if the Service is still connecting.
		{
			stop();
		}
		catch(StatusException e)
		{}
	}
	
	private void send(byte[] data, boolean flush) throws ConnectionError, StatusException
	{
		if(mStatus != ConnectionStatus.Connected && mStatus != ConnectionStatus.Handshake)
			throw new StatusException();
		
		if(mSocket != null)
		{
			try
			{
				mSocket.getOutputStream().write(data);
				if(flush)
					mSocket.getOutputStream().flush();
			}
			catch(IOException e)
			{
				onError();
				throw new ConnectionError();
			}
		}
	}
	
	@Override
	protected void doEmit(Event event)
	{
		try
		{
			send(event.bytes(), false);
		}
		catch(ConnectionError e)
		{}
		catch(StatusException e)
		{}
	}
	
	@Override
	protected void doSyn()
	{
		byte[] eol = { 0 };
		
		try
		{
			send(eol, true);
		}
		catch(ConnectionError e)
		{}
		catch(StatusException e)
		{}
	}
	
	protected void onReceive(String data)
	{
		if(mGamepad != null)
			mGamepad.onConnectionReceive(data);
	}
	
	public void addEventSource(EventSource source) throws StatusException
	{
		if(mStatus != ConnectionStatus.Handshake)
			throw new StatusException();
		
		mSources.add(source);
		source.setReceiver(this);
		
		Event[] events = source.getEventDefinitions();
		
		ByteBuffer buffer = ByteBuffer.allocate(3 * events.length);
		
		for(int i = 0; i < events.length; i++)
		{
			Event event = events[i];
			
			buffer.put(event.getType());
			buffer.putShort(event.getKey());
		}
		
		try
		{
			send(buffer.array(), false);
		}
		catch(ConnectionError e)
		{}
	}
	
	@Override
	public void feedback()
	{
		mGamepad.feedback();
	}
	
	public void forward() throws StatusException
	{
		if(mStatus != ConnectionStatus.Handshake)
			throw new StatusException();
		
		byte[] eol = { 0 };
		
		try
		{
			send(eol, true);
		}
		catch(ConnectionError e)
		{}
		
		mStatus = ConnectionStatus.Connected;
	}
	
	public void reset()
	{
		for(EventSource source : mSources)
			source.reset();
	}
	
	public void start(GamePad gamepad, ISocket socket) throws StatusException
	{
		if(this.mStatus != ConnectionStatus.NotConnected)
			throw new StatusException();
		
		mGamepad = gamepad;
		mSocket = socket;
		
		mSources.clear();
		
		Thread connectThread = new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				try
				{
					mSocket.connect();
					
					OutputStream stream = mSocket.getOutputStream();
					
					stream.write(mSocket.getHostName().getBytes());
					stream.write("\0".getBytes());
					stream.flush();
				}
				catch(IOException e)
				{
					// to make sure the gamepad gets notified
					mStatus = ConnectionStatus.Connected;
					mGamepad.runOnUiThread(new Runnable()
					{
						@Override
						public void run()
						{
							onError();
						}
					});
					return;
				}
				
				mStatus = ConnectionStatus.Handshake;
				
				mReaderThread = new ReaderThread();
				mReaderThread.start();
				
				mGamepad.runOnUiThread(new Runnable()
				{
					@Override
					public void run()
					{
						mGamepad.onConnectionCreated();
					}
				});
			}
		});
		connectThread.start();
	}
	
	public void stop() throws StatusException
	{
		
		if(this.mStatus == ConnectionStatus.NotConnected)
			return;
		
		if(this.mStatus == ConnectionStatus.Connecting)
			throw new StatusException("Can't stop service while connecting!");
		
		this.mStatus = ConnectionStatus.NotConnected;
		
		mSources.clear();
		
		try
		{
			if(mSocket != null)
				mSocket.close();
		}
		catch(IOException e)
		{}
		if(mGamepad != null)
			mGamepad.onConnectionLost();
	}
}
